'use strict';
var path = require('path');
var fs = require('fs');
var _ = require('lodash');

var extensions = _.chain(require.extensions)
    .keys()
    .unshift('')
    .value();

var checkParameterIncludePaths = function (includePaths) {
    if (!_.isArray(includePaths)) {
        throw new TypeError('The parameter <includePaths> should be an array', 'PARAMS_INVALID');
    }
    if (!includePaths.length) {
        throw new TypeError('The parameter <includePaths> should not be empty', 'PARAMS_INVALID');
    }
    var hasInvalidPath = _.some(includePaths, function (includePath) {
        return !_.isString(includePath);
    });
    if (hasInvalidPath) {
        throw new TypeError('The parameter <includePaths> should not contain invalid path, please check the path in include paths', 'PARAMS_INVALID');
    }
};
var checkParameterLoadedModules = function (loadedModules) {
    if (!_.isObject(loadedModules)) {
        if (!_.isUndefined(loadedModules) && !_.isNull(loadedModules)) {
            throw new TypeError('The parameter <loadedModules> should be an object or undefined or null', 'PARAMS_INVALID');
        }
        loadedModules = {};
    }
    return loadedModules;
};

var checkParameterParentInjector = function (parentInjector) {
    if (!_.isObject(parentInjector)) {
        if (!_.isUndefined(parentInjector) && !_.isNull(parentInjector)) {
            throw new TypeError('The parameter <parentInjector> should be an instance of <Injector> or undefined or null', 'PARAMS_INVALID');
        }
    } else if (!(parentInjector instanceof Injector)) {
        throw new TypeError('The parameter <parentInjector> should be an instance of <Injector>', 'PARAMS_INVALID');
    }
};

var isValidModuleName = (function () {
    var moduleNameRegex = new RegExp('^[0-9a-z_-]+([/][0-9a-z_-]+)*$', 'i');
    return function (moduleName) {
        return _.isString(moduleName) && moduleNameRegex.test(moduleName);
    };
}());

var checkParameterModuleName = function (moduleName) {
    if (!isValidModuleName(moduleName)) {
        throw new TypeError('The parameter <moduleName> should be a valid module name, please check it', 'PARAMS_INVALID');
    }
};


function ModuleDefinition(dependencies, factory) {
    this.load = function (loader) {
        var args = _.map(dependencies, function (moduleName) {
            return loader(moduleName);
        });
        return factory.apply(this, args);
    };
}

function Injector(includePaths, loadedModules, parentInjector) {
    checkParameterIncludePaths(includePaths);
    loadedModules = checkParameterLoadedModules(loadedModules);
    checkParameterParentInjector(parentInjector);

    var inject = function (moduleDefinition, sourceModuleName) {
        return moduleDefinition.load(function (moduleName) {
            if (!this.load(moduleName)) {
                if (sourceModuleName) {
                    throw new Error('The module "' + moduleName + '" is not found when loading module "' + sourceModuleName + '"!', 'MODULE_NOT_FOUND');
                } else {
                    throw new Error('The module "' + moduleName + '" is not found!', 'MODULE_NOT_FOUND');
                }
            }
            return this.getModule(moduleName);
        }.bind(this));
    }.bind(this);

    this.getModule = function (moduleName) {
        checkParameterModuleName(moduleName);
        if (moduleName in loadedModules) {
            return loadedModules[moduleName];
        } else if (parentInjector) {
            return parentInjector.getModule(moduleName);
        }
    };

    this.load = function (moduleName) {
        checkParameterModuleName(moduleName);
        if (!(moduleName in loadedModules)) {
            var filename = _.chain(includePaths)
                .map(function (includePath) {
                    return _.map(extensions, function (ext) {
                        return path.join(includePath, moduleName + ext);
                    });
                }).flatten()
                .find(function (filename) {
                    try {
                        fs.statSync(filename);
                    } catch (err) {
                        return false;
                    }
                    return true;
                }).value();
            if (filename) {
                var moduleDefinition = require(filename);
                loadedModules[moduleName] = moduleDefinition instanceof ModuleDefinition ? inject(moduleDefinition, moduleName) : moduleDefinition;
            } else {
                if (parentInjector && parentInjector.load(moduleName)) {
                    loadedModules[moduleName] = parentInjector.getModule(moduleName);
                } else {
                    return false;
                }
            }
        }
        return true;
    };

    this.inject = function (moduleDefinition) {
        if (!_.isObject(moduleDefinition) || !(moduleDefinition instanceof ModuleDefinition)) {
            throw new TypeError('The parameter <moduleDefinition> should be an instanceof <ModuleDefinition> that created by <Injector.define>', 'PARAMS_INVALID');
        }
        return inject(moduleDefinition);
    };

    this.defineAndInject = function (dependencies, factory) {
        var moduleDefinition = Injector.define(dependencies, factory);
        return this.inject(moduleDefinition);
    };
}



Injector.define = function (dependencies, factory) {
    if (!_.isArray(dependencies)) {
        throw new TypeError('The parameter <dependencies> should be an array', 'PARAMS_INVALID');
    }
    if (dependencies.length) {
        var hasInvalidModuleName = _.some(dependencies, function (moduleName) {
            return !isValidModuleName(moduleName);
        });
        if (hasInvalidModuleName) {
            throw new TypeError('The parameter <dependencies> should not contain invalid module name, please check the module name in <dependencies>', 'PARAMS_INVALID');
        }
    }
    if (!_.isFunction(factory)) {
        throw new TypeError('The parameter <factory> should be a function', 'PARAMS_INVALID');
    }
    return new ModuleDefinition(dependencies, factory);
};

module.exports = Injector;
